﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using SharePoint.Contrib.Extensions;
using SharePoint.Contrib.InversionOfControl;

namespace SharePoint.Contrib.WebApplication.Configuration
{
    public class WebApplicationConfigurationService
    {
        private SPFeature feature;
        private IEnumerable<XElement> nodes;
        private uint sequence = 0;
        private SPWebApplication webapplication;

        private void Trace(string message, params object[] args)
        {
            string msg = string.Format("WebApplicationConfigurationService: {0}", message);

            SPCServiceLocator.Current
                .DiagnosticsService
                .Trace(Constants.Areas.SPC, Constants.Categories.Services, message, args);
        }

        /// <summary>
        /// Get or Set the ConfigurationMode to use.
        /// </summary>
        public ConfigurationMode Mode { get; set; }

        public WebApplicationConfigurationService(SPFeature feature)
        {
            if (feature.Definition.Scope != SPFeatureScope.WebApplication)
            {
                throw "WebApplicationConfigurationService: The feature is not scoped for webapplication level which is the only scope that is supported."
                        .AsException();
            }
            this.feature = feature;
            Trace("Called from feature '{0}'.", this.feature.Name());

            this.webapplication = feature.Scope<SPWebApplication>(SPFeatureScope.WebApplication);   
        }

        public void ApplyChangesByFeature()
        {
            EnsureWebConfigFileExists();
            Trace("Applying changes from web.config file");

            CleanAttributesWithNamespaces();

            if (this.Mode == ConfigurationMode.Attributes)
            {
                Trace("Processing nodes: SPWebConfigModificationType.EnsureAttribute");
                AddModifications(from node in nodes
                                 where node.HasAttributes
                                 select node,
                                    SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute);
            }
            else
            {
                Trace("Processing nodes: SPWebConfigModificationType.EnsureSection");
                AddModifications(from node in nodes
                                 where node.HasAttributes == false
                                 select node,
                                SPWebConfigModification.SPWebConfigModificationType.EnsureSection);

                Trace("Processing nodes: SPWebConfigModificationType.EnsureChildNode");
                AddModifications(from node in nodes
                                 where node.HasAttributes
                                 select node,
                                SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            }

            CommitChanges();
        
        }

        public void RemoveChanges()
        {
            Trace("Removing all modifications made by owner '{0}'", this.feature.Identifier());
            RemoveConfigurationChanges(Modification => Modification.Owner == this.feature.Definition.Id.ToString());
        }

        /// <summary>
        /// Removes all modifications that matches the Func delegate.
        /// </summary>
        /// <param name="match">The match delegate</param>
        public void RemoveConfigurationChanges(Func<SPWebConfigModification, bool> match)
        {
            // filter out all results that are not sections (that is to buggy)
            var result = webapplication.WebConfigModifications
                            .Where(Config => Config.Type != SPWebConfigModification.SPWebConfigModificationType.EnsureSection)
                            .Where(match).ToArray();


            foreach (var modification in result)
            {
                try
                {
                    Trace("Removing modification '{0}'", modification.Info());
                    webapplication.WebConfigModifications.Remove(modification);
                }
                catch (Exception e)
                {
                    Trace("An error occured when removing modification '{0}'. This modification was not removed. Exception details: {1}",
                                        modification.Info(), e.ToString());
                    continue;
                }
            }

            CommitChanges();

        }

        private void AddModifications(IEnumerable<XElement> nodes, SPWebConfigModification.SPWebConfigModificationType modificationType)
        {

            foreach (var node in nodes)
            {
                Trace("Processing modification on node '{0}'", node.Name.LocalName);

                SPWebConfigModification modification = new SPWebConfigModification
                {
                    Path = BuildXPathFrom(node),
                    Sequence = sequence++,
                    Owner = feature.Identifier(),
                };

                switch (modificationType)
                {
                    case SPWebConfigModification.SPWebConfigModificationType.EnsureSection:
                        modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureSection;
                        modification.Value = node.Name.LocalName;
                        modification.Name = node.Name.LocalName;
                        break;
                    case SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode:
                        modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
                        modification.Value = node.ToString(SaveOptions.DisableFormatting);
                        modification.Name = string.Format("{0}[@{1}='{2}']", node.Name.LocalName, node.FirstAttribute.Name.LocalName, node.FirstAttribute.Value);
                        break;
                    case SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute:
                        modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute;
                        modification.Value = node.FirstAttribute.Value;
                        modification.Name = node.FirstAttribute.Name.LocalName;
                        break;

                }
                if (webapplication.WebConfigModifications.Any(Config => Config.Equals(modification)))
                {
                    continue;
                }
                Trace("Adding node '{0}' as '{1}'", node.Name.LocalName, modification.Info());
                webapplication.WebConfigModifications.Add(modification);
            }
        }
        
        private void CommitChanges()
        {
            try
            {
                Trace("Committing changes");
                webapplication.Farm.Services.GetValue<SPWebService>(webapplication.Parent.Id).ApplyWebConfigModifications();
                webapplication.Update();
                Trace("All updates committed successfully");
            }
            catch (Exception e)
            {
                throw "WebApplicationConfigurationService: Failed to commit web.config changes."
                        .AsException(e);
            }
        }

        private void EnsureWebConfigFileExists()
        {
            try
            {
                var file = feature
                            .LocateElementFile("web.config")
                            .AsXml();

                nodes = file
                            .Root
                            .Descendants()
                            .ToArray();
            }
            catch (Exception e)
            {
                throw "WebApplicationConfigurationService: The feature does not include a web.config file"
                        .AsException(e);
            }
        }

        private void CleanAttributesWithNamespaces()
        {
            List<XElement> ToRemove = new List<XElement>();

            var NamespaceAttributeNodes = from attribute in nodes.Attributes()
                                          where attribute.IsNamespaceDeclaration
                                          let node = attribute.Parent
                                          select new
                                          {
                                              Node = node,
                                              Namespace = attribute.Value,
                                              WithoutNamespace = node.RemoveNamespace(attribute.Name.LocalName)
                                          };

            foreach (var item in NamespaceAttributeNodes)
            {
                Trace("Processing attribute '{0}' with namespace '{1}'", item.Node.Name.LocalName, item.Namespace);

                var path = string.Format("{0}/*[namespace-uri()='{1}' and local-name()='{2}']",
                            BuildXPathFrom(item.Node),
                            item.Namespace,
                            item.WithoutNamespace.Name.LocalName);

                Trace(" Namespace attribute was found. XPath to node '{0}' changed to '{1}'.", item.Node.Name.LocalName, path);

                var children = from child in item.WithoutNamespace.Elements()
                               where child.HasAttributes == false
                               select child;

                foreach (var child in children)
                {

                    var firstNode = child.Elements().FirstOrDefault();

                    if (firstNode == null)
                    {
                        continue;
                    }

                    var firstAttribute = firstNode.FirstAttribute;

                    if (firstAttribute == null)
                    {
                        continue;
                    }

                    SPWebConfigModification modification = new SPWebConfigModification
                    {
                        Path = path,
                        Sequence = sequence++,
                        Owner = feature.Definition.Id.ToString(),
                        Name = string.Format("*[namespace-uri()='{0}' and local-name()='{1}']/*[namespace-uri()='{0}' and local-name()='{2}'][@{3}='{4}']/parent::*",
                                item.Namespace,
                                child.Name.LocalName,
                                firstNode.Name.LocalName,
                                firstAttribute.Name.LocalName,
                                firstAttribute.Value),
                        Value = child.ToString(SaveOptions.DisableFormatting)
                    };


                    if (webapplication.WebConfigModifications.Any(Config => Config.Equals(modification)))
                    {
                        continue;
                    }

                    Trace("Adding modication: '{0}'", modification.Info());
                    webapplication.WebConfigModifications.Add(modification);
                }

                // remove the node from the xml so it won´t get handled again
                ToRemove.Add(item.Node);
            }

            ToRemove.ForEach(node =>
            {
                node.Remove(); // remove from parent
            });

        }

        private string BuildXPathFrom(XElement node)
        {
            string NodeName = node.Name.LocalName;
            var XPathTree = new List<string>();

            // build XPath tree from the node to the top of the configuration file
            while (node != null)
            {
                if (node.HasAttributes && this.Mode == ConfigurationMode.Full)
                {
                    XPathTree.Add(string.Format("{0}[@{1}='{2}']", node.Name.LocalName, node.FirstAttribute.Name.LocalName, node.FirstAttribute.Value));
                }
                else
                {
                    XPathTree.Add(node.Name.LocalName);
                }
                node = node.Parent;
            }

            // from top to bottom (reverse it)
            XPathTree.Reverse();

            // if it is not 'attributes only' we will not include the actual node
            if (this.Mode == ConfigurationMode.Full && XPathTree.Count > 1)
            {
                XPathTree.RemoveAt(XPathTree.Count - 1);
            }

            string XPathString = string.Join("/", XPathTree.ToArray());
            Trace("XPath to node '{0}' = '{1}'", NodeName, XPathString);

            // special care if we have remove or clear nodes
            if (NodeName.Contains("remove") || NodeName.Contains("clear"))
            {
                XPathString = string.Concat("/", XPathString);
                Trace(" Found 'remove' or 'clear' node that must appear first. XPath to node '{0}' changed to '{1}'.", NodeName, XPathString);
            }

            return XPathString;
        }

        public enum ConfigurationMode
        {
            Full,
            Attributes
        }
    }



}
